package file

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"

	"os"
	"time"
)

var BadBlock = fmt.Errorf("Corrupted block found aborting reads")

func serialize(w io.Writer, v interface{}) error {
	return binary.Write(w, binary.LittleEndian, v)
}

func readBlockHeader(r io.Reader) (h BlockHeader, err error) {
	s := make([]byte, sentinelSize)
	err = binary.Read(r, binary.LittleEndian, s)
	if err != nil {
		return h, err
	}
	if !bytes.Equal(s, sentinel) {
		return h, BadBlock
	}
	err = binary.Read(r, binary.LittleEndian, &h)
	if err == nil && h.Size < 0 {
		return h, BadBlock
	}
	return h, nil
}

// TODO(jwall): figure out how to safely update series out of order.

// writeSeries writes timeseries data in a block to an io.Writer.
func writeSeries(w io.ReadWriteSeeker, dps []Data, blockSize int32) error {
	// TODO(jwall): Should writeSeries understand block size and
	// append to a block if it's not full yet.
	dpsLen := int32(len(dps))
	if dpsLen < 1 {
		return fmt.Errorf("Can't write timeseries data of size 0")
	}
	ndps := make([]Data, blockSize)
	// TODO(jwall): split the block if the data to write is larger than
	// blockSize.
	if dpsLen < blockSize {
		copy(ndps, dps)
	} else {
		ndps = dps
	}
	span := time.Unix(0, dps[len(dps)-1].Ts).Sub(time.Unix(0, dps[0].Ts))
	err := writeBlockHeader(w, BlockHeader{Start: dps[0].Ts, Span: span, Size: int32(blockSize)})
	if err != nil {
		return err
	}
	err = binary.Write(w, binary.LittleEndian, ndps)
	if err != nil {
		return err
	}
	return nil
}

func writeBlockHeader(w io.Writer, bh BlockHeader) error {
	b := new(bytes.Buffer)
	binary.Write(b, binary.LittleEndian, sentinel)
	binary.Write(b, binary.LittleEndian, bh)
	_, err := w.Write(b.Bytes())
	return err
}

func readTimespanFromSeeker(f io.ReadSeeker, blockSize int32) (time.Time, time.Time, error) {
	first, err := readFirst(f)
	if err != nil || first == nil {
		return zeroTime, zeroTime, fmt.Errorf("Error reading start from file %q", err)
	}
	last, err := readLast(f, blockSize)
	if err != nil || last == nil {
		return zeroTime, zeroTime, fmt.Errorf("Error reading end from file %q", err)
	}
	//log.Printf("first %v last %v", first, last)
	return time.Unix(0, first.Ts), time.Unix(0, last.Ts), nil
}

func readTimespan(path string, blockSize int32) (time.Time, time.Time, error) {
	f, err := os.Open(path)
	if err != nil {
		return zeroTime, zeroTime, fmt.Errorf("Error opening file %q", err)
	}
	defer f.Close()
	return readTimespanFromSeeker(f, blockSize)
}

func readLastBlock(r io.ReadSeeker, blockSize int32) (BlockHeader, []Data, error) {
	_, err := r.Seek(-int64(((dpSize * blockSize) + bhSize + sentinelSize)), 2)
	if err == nil || err == io.EOF {
		bh, err := readBlockHeader(r)
		if err != nil && err != io.EOF {
			return zeroBh, nil, err
		}
		data, err := readData(r, bh)
		if err != nil && err != io.EOF {
			return zeroBh, nil, err
		}
		return bh, data, nil
	}
	return zeroBh, nil, err
}

func readLast(r io.ReadSeeker, blockSize int32) (*Data, error) {
	_, ds, err := readLastBlock(r, blockSize)
	if err != nil && err != io.EOF {
		return nil, err
	}
	if ds == nil {
		return nil, fmt.Errorf("No last data")
	}
	for i := len(ds) - 1; i > 0; i-- {
		if ds[i] != zeroDp {
			d := ds[i]
			return &d, err
		}
	}
	return nil, err
}

func readFirst(r io.ReadSeeker) (*Data, error) {
	if _, err := r.Seek(0, 0); err == nil || err == io.EOF {
		_, err := readBlockHeader(r)
		if err != nil {
			return nil, err
		}
		dp := Data{}
		err = binary.Read(r, binary.LittleEndian, &dp)
		if err == nil {
			return &dp, nil
		}
		return nil, err
	} else {
		return nil, err
	}
}

func writeAll(w io.ReadWriteSeeker, dps []Data, blockSize int32) {
	for {
		if int32(len(dps)) < blockSize {
			break
		}
		var data []Data
		data, dps = dps[:blockSize], dps[blockSize:]
		writeSeries(w, data, blockSize)
	}
	// handle any leftovers
	if len(dps) > 0 {
		writeSeries(w, dps, blockSize)
	}
}

func readAll(r io.ReadSeeker) ([]Data, error) {
	var dps []Data
	r.Seek(0, 0)
	for {
		bh, err := readBlockHeader(r)
		if err != nil {
			return dps, err
		}
		data, err := readData(r, bh)
		dps = append(dps, data...)
		if err != nil {
			return dps, err
		}
	}
}

// readseries reads a series from an io.Reader. It searches the reader for
// datapoints at the start time and reads until it's reached the end of the
// duration specified. Returns an error seen while reading the stream.
func readSeries(r io.ReadSeeker, start time.Time, span time.Duration) ([]Data, error) {
	spanEnd := start.Add(span)
	// TODO(jwall): optimize by sizing slice capacity to something intelligent
	// based off of span duration.
	var dps []Data
	for {
		bh, err := readBlockHeader(r)
		if err != nil {
			return dps, err
		}
		tmp, err := readDatas(r, bh, start, spanEnd)
		if bh.Size < 0 {
			return nil, BadBlock
		}
		if err != nil {
			return dps, err
		}
		if len(tmp) > 0 {
			dps = append(dps, tmp...)
		}
	}
	return dps, nil
}

func readData(r io.ReadSeeker, bh BlockHeader) ([]Data, error) {
	buf := make([]Data, bh.Size)
	return buf, binary.Read(r, binary.LittleEndian, &buf)
}

func readDatas(r io.ReadSeeker, bh BlockHeader, start, spanEnd time.Time) ([]Data, error) {
	cursor := time.Unix(0, bh.Start)
	endNano := spanEnd.UnixNano()
	startNano := start.UnixNano()
	var dps []Data
	if cursor.After(spanEnd) {
		return dps, TimeSpanExceeded{spanEnd, cursor}
	}
	if cursor.Add(bh.Span).Before(start) {
		// Skip this block.
		if _, err := r.Seek(int64(bh.Size*dpSize+sentinelSize), 1); err != nil {
			return dps, err
		}
	}
	buf, err := readData(r, bh)
	if err != nil && err != io.EOF {
		return dps, err
	}
	if buf == nil {
		return nil, fmt.Errorf("No data in block")
	}
	for _, dp := range buf {
		if dp.Ts >= startNano && dp.Ts <= endNano && dp != zeroDp {
			//log.Printf("Found entry for timestamp %d", dp.Ts)
			dps = append(dps, dp)
		}
	}
	return dps, err
}
